跳到主要内容

Go 的 gomock 工具

转载自 GoMock 快速上手教程 (部分过时的部分进行了修改)

gomock 是什么?

GoMock 是 Go 语言官方出品的一款 mock 框架

安装 GoMock

首先,我们需要安装 gomock 包 和代码生成工具 mockgen。准确来说,即使不安装 mockgen 我们依然可以使用 GoMock,但是那样的话就需要我们自己来写 mock 代码,这样做不仅麻烦而且很容易出错。

gomock 和 mockgen 均可以使用 go get 来安装,具体命令如下:

go get github.com/golang/mock/gomock

go install github.com/golang/mock/mockgen@v1.6.0

可以通过执行如下命令来验证 mockgen 是否已经成功安装:

mockgen

项目上要添加依赖

go get github.com/golang/mock/mockgen/model

基本用法

  • GoMock 的使用通常遵循如下四个基本步骤:
  • 使用 mockgen 为你想要 mock 的接口生成一个 mock。
  • 在你的测试代码中,创建一个 gomock.Controller 实例并把它作为参数传递给 mock 对象的构造函数来创建一个 mock 对象。
  • 调用 EXPECT() 为你的 mock 对象设置各种期望和返回值。
  • 调用 mock 控制器的 Finish() 以验证 mock 的期望行为。

通过一个简单的例子来演示 GoMock 的整个使用流程,为简单起见我们只看两个文件,一个是文件 doer/doer.go 中我们希望 mock 的接口 Doer,另一个是文件 user/user.go 中使用了 Doer 接口的结构 User。

我们想要 mock 的接口只有几行代码,这个接口有一个 DoSomething 方法,该方法接受 int 和 string 类型的参数并返回一个 error。

// doer/doer.go

package doer

type Doer interface {
DoSomething(int, string) error
}

下面是我们想 mock 掉 Doer 接口进行测试的代码:

// user/user.go

package user

import "stmock/doer"

type User struct {
Doer doer.Doer
}

func (u *User) Use() error {
return u.Doer.DoSomething(123, "Hello GoMock")
}

我们当前的项目代码结构如下:

-- doer
-- doer.go
-- user
-- user.go

我们会把 Doer 接口的 mock 代码放在项目根目录下的 mocks 包中,然后把 User 的测试代码放在 user/user_test.go 中。

-- doer
-- doer.go
-- mocks
-- mock_doer.go
-- user
-- user.go
-- user_test.go

我们首先创建一个用于放置 mock 实现的目录 mocks 然后针对 doer 包执行 mockgen 命令。

mockgen -destination=mocks/mock_doer.go -package=mocks stmock/doer Doer

也可以这样写,不过这样就是以当前目录为根路径创建的

生成的代码

package mocks

import (
reflect "reflect"

gomock "github.com/golang/mock/gomock"
)

// MockDoer is a mock of Doer interface.
type MockDoer struct {
ctrl *gomock.Controller
recorder *MockDoerMockRecorder
}

// MockDoerMockRecorder is the mock recorder for MockDoer.
type MockDoerMockRecorder struct {
mock *MockDoer
}

// NewMockDoer creates a new mock instance.
func NewMockDoer(ctrl *gomock.Controller) *MockDoer {
mock := &MockDoer{ctrl: ctrl}
mock.recorder = &MockDoerMockRecorder{mock}
return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockDoer) EXPECT() *MockDoerMockRecorder {
return m.recorder
}

// DoSomething mocks base method.
func (m *MockDoer) DoSomething(arg0 int, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DoSomething", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}

// DoSomething indicates an expected call of DoSomething.
func (mr *MockDoerMockRecorder) DoSomething(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoSomething", reflect.TypeOf((*MockDoer)(nil).DoSomething), arg0, arg1)
}

注意这里自动生成的 EXPECT() 方法与要 mock 的方法(在本示例代码中就是 DoSomething)是定义在同一个对象上的,所以 EXPECT() 方法采用全大写命名可能是为了避免名字冲突(比如要 mock 的接口可能有一个 Expect 方法)。

接下来在我们的测试代码中要定义一个 mock controller。mock controller 负责追踪和验证所有与它关联的 mock 对象的期望。

我们可以通过传递一个类型为 testing.T* 的值 t 给 mock controller 的构造函数来获取一个 mock controller 对象,然后使用它来构建一个 Doer 接口的 mock。我们还需要通过 defer 的方式来调用 mock controller 的 Finish 方法,关于Finish 方法后文会有更详细的介绍。

mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

mockDoer := mocks.NewMockDoer(mockCtrl)

假定我们要断言 mockDoer 的 Do 会被调用一次,调用时传入的参数是 123 和 Hello GoMock,返回值是 nil

为此,在测试用例中我们可以调用 mockDoer 的 EXPECT() 来设置期望,EXPECT() 会返回一个我们称之为 mock recorder 的对象,这个对象提供了 Doer 接口的所有方法。

调用 mock recorder 的任意方法也就根据给定的参数指定了一次期望的调用。你可以在调用之后继续链式调用其他属性,比如:

  • 通过 .Return(...) 指定返回值
  • 通过 .Times(number).MaxTimes(number) 以及 .MinTimes(number) 来指定该调用的期望次数

在本例中,我们的调用就是这样的:

mockDoer.EXPECT().DoSomething(123, "Hello GoMock").Return(nil).Times(1)

这样我们就完成第一个 mock 调用,以下是完整的代码示例:

// user/user_test.go

package user_test

import (
"github.com/golang/mock/gomock"
"stmock/mocks"
"stmock/user"
"testing"
)

func TestUse(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

mockDoer := mocks.NewMockDoer(mockCtrl) // 调用之前生成的那个 Mock 的方法
testUser := &user.User{Doer: mockDoer}

// Expect Do to be called once with 123 and "Hello GoMock" as parameters, and return nil from the mocked call.
mockDoer.EXPECT().DoSomething(123, "Hello GoMock").Return(nil).Times(1)

testUser.Use()
}

从上的代码中我们可能不太容易看出 mock 的期望是在哪里断言的,其实这发生在通过 defer 延迟执行的 Finish() 函数里面。在 mock controller 声明的地方就延迟调用 Finish 是一种更为符合 Go 语言习惯的写法,这样可以避免之后我们忘记对 mock 的期望进行断言。

测试结果:

=== RUN   TestUse
--- PASS: TestUse (0.00s)
PASS

在实际测试中如果需要构建多个 mock,那么你可以重用 mock controller,它的 Finish 方法会对与之关联的所有 mock 的期望进行断言。

我们可能还想验证 Use 方法的返回值确实是 DoSomething 返回的,我们可以写另外一个测试,创建任意一个错误然后作为 mockDoer.DoSomething 的返回值:

func TestUseReturnsErrorFromDo(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

dummyError := errors.New("dummy error")
mockDoer := mocks.NewMockDoer(mockCtrl)
testUser := &user.User{Doer: mockDoer}

// Expect Do to be called once with 123 and "Hello GoMock" as parameters, and return dummyError from the mocked call.
mockDoer.EXPECT().DoSomething(123, "Hello GoMock").Return(dummyError).Times(1)

err := testUser.Use() // 调用了 User 的 Use() 方法

if err != dummyError {
t.Fail()
}
}

结合 go:generate 使用 GoMock

在实际开发中我们可能有很多接口或者包需要进行 mock,那么针对每个包或者接口单独运行 mockgen 来生成 mock 就比较麻烦了。为了解决这个问题,mockgen 命令可以放在一个特殊的 go:generate 注释中。

在我们的例子中,我们可以在 doer.go 文件的 package 声明下面添加一行 go:generate 注释:

package doer

//go:generate mockgen -destination=../mocks/mock_doer.go -package=mocks stmock/doer Doer

type Doer interface {
DoSomething(int, string) error
}

使用参数匹配

有些时候你可能并不太关心调用 mock 时指定的参数。使用 GoMock,参数既可以是一个确定的值又可以是一个满足某种条件的匹配,后者被称为一个 Matcher。

Matcher 用来代表一个 mock 的方法可以接受的某个范围内的参数,以下是 GoMock 中一些预定义的 matcher:

gomock.Any():匹配任何类型的任何值 gomock.Eq(x):匹配使用反射 reflect.DeepEqual 与 x 相等的值 gomock.Nil():匹配等于 nil 的值 gomock.Not(m):(这里的 m 是一个 Matcher)匹配同 m 不匹配的值 gomock.Not(x):(这里的 x 不是 Matcher)匹配使用反射 reflect.DeepEqual 与 x 不相等的值

联系例子来说明,就是如果我们不关心 Do 函数的第一个参数值,那么我们可以这么写:

mockDoer.EXPECT().DoSomething(gomock.Any(), "Hello GoMock")

GoMock 会自动将不是 Matcher 类型的参数转换为 Eq 这种 matcher,所以上面的代码与下面的等价:

mockDoer.EXPECT().DoSomething(gomock.Any(), gomock.Eq("Hello GoMock"))

另外你也可以通过实现 gomock.Matcher 接口来定义自己的 matcher,gomock.Matcher 接口的定义如下:

//gomock/matchers.go

type Matcher interface {
Matches(x interface{}) bool
String() string
}

Matches 方法是真正用来做匹配的,而 String 方法在测试失败时用来生成人类可读的输出。比如一个用来检查参数类型的自定义 matcher 可以通过如下方式来实现:

// match/oftype.go

package match

import (
"reflect"
"github.com/golang/mock/gomock"
)

type ofType struct{ t string }

func OfType(t string) gomock.Matcher {
return &ofType{t}
}

func (o *ofType) Matches(x interface{}) bool {
return reflect.TypeOf(x).String() == o.t
}

func (o *ofType) String() string {
return "is of type " + o.t
}

我们可以通过下面代码的方式来使用这个自定义的 matcher,即期望 DoSomething 被调用一次,调用的参数是 123 和任意一个 string 类型的值,返回值是 nil。

mockDoer.EXPECT().
DoSomething(123, match.OfType("string")).
Return(nil).
Times(1)

断言调用顺序

一个对象的调用顺序通常是很重要的,调用顺序不符合预期往往代表程序是有问题的。GoMock 提供了一种确保某个调用必需发生在另外一个调用之后的机制,那就是 .After 方法,以下为示例,其指定 callFirst 方法必需在 callA 或者 callB 之前被调用。

callFirst := mockDoer.EXPECT().DoSomething(1, "first this")
callA := mockDoer.EXPECT().DoSomething(2, "then this").After(callFirst)
callB := mockDoer.EXPECT().DoSomething(2, "or this").After(callFirst)

GoMock还提供了另外一个更便捷的方法来指定不同调用之间的先后顺序,那就是 gomock.InOrder。它使用起来不如 .After 灵活,但是可以使得较长的一串调用顺序看起来更清晰。

gomock.InOrder(
mockDoer.EXPECT().DoSomething(1, "first this"),
mockDoer.EXPECT().DoSomething(2, "then this"),
mockDoer.EXPECT().DoSomething(3, "then this"),
mockDoer.EXPECT().DoSomething(4, "finally this"),
)

指定 mock 行为

mock 对象与真正的接口实现不同,它们不实现任何接口方法,只是在被调用的时候返回已经定义好的响应并记录调用行为

然而你可能需要你的 mock 能做的更多,这时我们就可以使用 GoMock 提供的 Do 方法了。任何 mock 调用都可以通过 .Do 来绑定一个函数,这个函数在 mock 真正被调用时就会自动执行。

mockDoer.EXPECT().
DoSomething(gomock.Any(), gomock.Any()).
Return(nil).
Do(func(x int, y string) {
fmt.Println("Called with x =",x,"and y =", y)
})

关于调用参数的一些复杂验证逻辑可以写在 .Do 的函数中,例如 DoSomething 的第一个 int 类型的参数应该小于等于第二个 string 类型参数的长度,那么我们就可以这样来实现:

mockDoer.EXPECT().
DoSomething(gomock.Any(), gomock.Any()).
Return(nil).
Do(func(x int, y string) {
if x > len(y) {
t.Fail()
}
})

相同的功能我们通过自定义 matcher 是无法实现的,因为 matcher 只能针对一个参数实现相关的匹配逻辑,无法处理多个不同参数值间的关联关系。